package AddonInfo;

use strict;
use warnings;

use Getopt::Long;
use IO::File;
use Text::ParseWords;
use URI;

use Constants qw( :dir :source );
use Options;
use Database;
use FormatString;
use AddonSVN;
use AddonKey;

BEGIN {
    use Exporter ();
    our ( $VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS );

    $VERSION     = sprintf( "%d", q$Revision$ =~ m{\d+} );
    @ISA         = qw(Exporter);
    @EXPORT      = qw();
    %EXPORT_TAGS = ();
    @EXPORT_OK   = qw();
} ## end BEGIN

# Flags
use constant field_name          => 0;
use constant field_key           => 1;
use constant field_internal      => 2;
use constant field_override      => 3;
use constant field_record_type   => 4;
use constant field_source        => 5;
use constant field_url           => 6;
use constant field_svn_url       => &field_url;
use constant field_git_url       => &field_url;
use constant field_hg_url        => &field_url;
use constant field_require       => 7;
use constant field_base_key      => 8;
use constant field_alias         => 9;
use constant field_is_assumption => 10;
use constant field_is_dead       => 11;

use constant default_fields      => "fields";
use constant default_addons      => "addons";
use constant default_hide_addons => "hide_addons";
use constant default_dummy       => "dummy";
use constant default_group       => "group";
use constant default_require     => "require";
use constant default_alias       => "alias";
use constant default_split       => "split";
use constant default_libname     => "libname";
use constant default_can_cache   => "cancache";

# Types
use constant record_type_addon   => "ADDON";
use constant record_type_dummy   => "DUMMY";
use constant record_type_split   => "SPLIT";
use constant record_type_libname => "LIBNAME";
use constant record_type_alias   => "ALIAS";

# Source types are defined in Constants.pm

our %record_type_rank;
our %groups_by_name            = ();
our %groups_by_addon           = ();
our %addons_by_key             = ();
our %externals_embedded_by_key = ();
our %split_allowed_by_key      = ();
our %splitoffs_by_key          = ();
our %libnames_by_key           = ();
our %must_embed_urls_by_key    = ();
our %defaults;
our $loaded               = 0;
our %source_types_to_name = (
    &SOURCE_SVN      => "SVN",
    &SOURCE_GIT      => "GIT",
    &SOURCE_HG       => "Mercurial",
    &SOURCE_DOWNLOAD => "Manual",
);

## Package Function Prototypes
### Private
sub p_quote_word($ );

## Package Method Prototypes
### Private
sub p_make_entry($$$;@ );
sub p_find_entry($$ );
sub p_clear_entry($$ );

### Public
sub lookup($$ );
sub make_source_url($$$$ );
sub make_svn_url($$$ );
sub make_git_url($$$ );
sub make_hg_url($$$ );
sub make_download($$ );
sub make_dummy($$ );
sub make_alias($$@ );
sub remake_alias($$@ );
sub is_group($$ )  { $groups_by_name{ $_[ 1 ] }; }
sub get_groups($ ) { keys %groups_by_name; }
sub load_database($ );
sub save_database($ );

## Instance Method Prototypes
### Private
sub p_clear($ ) { $_[ 0 ]->p_clear_entry( $_[ 0 ]->[ field_key ] ); }
sub p_is_weaker_than($$ );
sub p_make_internal($ ) { $_[ 0 ]->[ field_internal ] = 1; $_[ 0 ] }
sub p_make_override($ ) { $_[ 0 ]->[ field_override ] = 1; $_[ 0 ] }
sub p_normalize_url($ );
sub p_expand_field($$ );
sub p_add_to_field($$@ );
### Public
sub get_name($ )        { $_[ 0 ]->[ field_name ]; }
sub is_internal($ )     { $_[ 0 ]->[ field_internal ]; }
sub is_override($ )     { $_[ 0 ]->[ field_override ]; }
sub get_record_type($ ) { $_[ 0 ]->[ field_record_type ]; }
sub is_dummy($ )        { $_[ 0 ]->get_record_type eq &record_type_dummy }
sub is_split($ )        { $_[ 0 ]->get_record_type eq &record_type_split }
sub is_libname($ )      { $_[ 0 ]->get_record_type eq &record_type_libname }
sub is_alias($ )        { $_[ 0 ]->get_record_type eq &record_type_alias }
sub get_source_type($ ) { $_[ 0 ]->[ field_source ]; }
sub get_url($;$ );
sub get_svn_url($ )    { return $_[ 0 ]->get_url( &SOURCE_SVN ); }
sub get_git_url($ )    { return $_[ 0 ]->get_url( &SOURCE_GIT ); }
sub get_hg_url($ )     { return $_[ 0 ]->get_url( &SOURCE_HG ); }
sub clear_require($ )  { delete $_[ 0 ]->[ field_require ]; }
sub add_require($;@ )  { shift->p_add_to_field( field_require, @_ ); }
sub expand_require($ ) { $_[ 0 ]->p_expand_field( field_require ); }
sub get_base_addon($ );
sub get_base_addoninfo($ );
sub embed_externals($ )    { set_externals_embedded( $_[ 0 ], 1 ); }
sub disembed_externals($ ) { set_externals_embedded( $_[ 0 ], 0 ); }
sub set_externals_embedded($$ );
sub are_externals_embedded($;$ );
sub allow_split($ )    { set_split_allowed( $_[ 0 ], 1 ); }
sub disallow_split($ ) { set_split_allowed( $_[ 0 ], 0 ); }
sub set_split_allowed($$ );
sub is_split_allowed($;$ );
sub add_split($;@ );
sub get_splits($ );
sub add_embed_urls($;@ );
sub remove_embed_urls($;@ );
sub get_embed_urls($ );
sub add_libname($;@ );
sub get_libnames($ );
sub get_alias($ ) { my $res = $_[ 0 ]->[ field_alias ]; return $res ? @$res : (); }
sub get_group($ );
sub set_group($$ );
sub is_assumption($ ) { $_[ 0 ]->[ field_is_assumption ]; }
sub make_assumption($ ) { $_[ 0 ]->[ field_is_assumption ] = 1; $_[ 0 ]; }
sub clear_if_assumption($ ) { $_[ 0 ]->p_clear if $_[ 0 ]->[ field_is_assumption ]; }
sub keep_assumption($ ) { delete $_[ 0 ]->[ field_is_assumption ]; $_[ 0 ]; }
sub is_dead($ ) { $_[ 0 ]->[ field_is_dead ]; }
sub make_dead($ )  { $_[ 0 ]->[ field_is_dead ] = 1; $_[ 0 ]; }
sub clear_dead($ ) { $_[ 0 ]->[ field_is_dead ] = 0; $_[ 0 ]; }
sub clear_dead_as_svn($$ );
sub clear_dead_as_git($$ );
sub clear_dead_as_hg($$ );
sub set_dead($$ ) { $_[ 0 ]->[ field_is_dead ] = $_[ 1 ]; $_[ 0 ]; }

# Public Prototypes
%record_type_rank = (
    &record_type_addon => 0,
    &record_type_dummy => 0,

    &record_type_split => 1,

    &record_type_libname => 2,

    &record_type_alias => 3,
);
%defaults = (
    "dummy" => {
        &default_dummy => [ "Icon\r", ".DS_Store", ".svn", ".git", ".hg", "Blizzard_" ],
    },
);

## Package Functions
### Private
sub p_quote_word($ ) {
    my $word = shift;
    if ( $word =~ s{([^%!\w\d:+,-./^_{|}~-])}{\\$1}g || $word eq "" ) {
        $word =~ s{\\\r}{\\r}g;
        return '"' . $word . '"';
    }
    else {
        return $word;
    }
} ## end sub p_quote_word($ )

## Package Methods
### Private
sub p_make_entry($$$;@ ) {
    my $this        = shift;
    my $name        = shift;
    my $record_type = shift;
    my $class       = ref( $this ) || $this;
    my $key         = addon_name_to_key( $name );
    my $res         = $addons_by_key{ $key };
    if ( $res ) {
        return undef unless ( $res->p_is_weaker_than( $record_type ) );
        $res->p_clear;
    }
    $res = [];
    while ( @_ ) {
        my $field = shift;
        my $value = shift;
        $res->[ $field ] = $value;
    }
    bless $res, $class;
    $res->[ field_name ]        = $name;
    $res->[ field_key ]         = $key;
    $res->[ field_record_type ] = $record_type;
    $addons_by_key{ $key }      = $res;
    return $res;
} ## end sub p_make_entry($$$;@ )

sub p_find_entry($$ ) {
    my $this = shift;
    my $name = shift;
    my $key  = addon_name_to_key( $name );
    return $addons_by_key{ $key };
} ## end sub p_find_entry($$ )

sub p_clear_entry($$ ) {
    my $this = shift;
    my $name = shift;
    my $key  = addon_name_to_key( $name );
    my $old  = delete $addons_by_key{ $key };
    if ( $old ) {
        my $old_type = $old->get_record_type;
        if ( $old_type eq record_type_split ) {
            my $old_key   = $old->[ field_base_key ];
            my $split_rec = $splitoffs_by_key{ $old_key };
            delete $split_rec->{ $key } if ( $split_rec );
        }
        elsif ( $old_type eq record_type_libname ) {
            my $old_key = $old->[ field_base_key ];
            my $lib_rec = $libnames_by_key{ $old_key };
            delete $lib_rec->{ $key } if ( $lib_rec );
        }
    } ## end if ( $old )
} ## end sub p_clear_entry($$ )

### Public
sub lookup($$ ) {
    my ( $class, $addon ) = @_;
    $class->load_database unless $loaded;
    return $class->p_find_entry( $addon );
}

sub make_source_url($$$$ ) {
    my ( $class, $name, $source_type, $url ) = @_;
    $class->load_database unless $loaded;
    return undef          unless defined $source_type;
    return undef          unless exists $source_types_to_name{ $source_type };
    return undef          unless defined $url;
    my $res = $class->p_make_entry( $name, record_type_addon );
    return undef unless $res;
    $res->[ field_source ] = $source_type;
    $res->[ field_url ]    = $url;
    return $res;
} ## end sub make_source_url($$$$ )

sub make_svn_url($$$ ) {
    my ( $class, $name, $url ) = @_;
    $class->load_database unless $loaded;
    my $res = $class->p_make_entry( $name, record_type_addon );
    return undef unless $res;
    $res->[ field_source ]  = &SOURCE_SVN;
    $res->[ field_svn_url ] = $url;
    return $res;
} ## end sub make_svn_url($$$ )

sub make_git_url($$$ ) {
    my ( $class, $name, $url ) = @_;
    $class->load_database unless $loaded;
    my $res = $class->p_make_entry( $name, record_type_addon );
    return undef unless $res;
    $res->[ field_source ]  = &SOURCE_GIT;
    $res->[ field_git_url ] = $url;
    return $res;
} ## end sub make_git_url($$$ )

sub make_hg_url($$$ ) {
    my ( $class, $name, $url ) = @_;
    $class->load_database unless $loaded;
    my $res = $class->p_make_entry( $name, record_type_addon );
    return undef unless $res;
    $res->[ field_source ] = &SOURCE_HG;
    $res->[ field_hg_url ] = $url;
    return $res;
} ## end sub make_hg_url($$$ )

sub make_download($$ ) {
    my ( $class, $name, $url ) = @_;
    $class->load_database unless $loaded;
    my $res = $class->p_make_entry( $name, record_type_addon );
    return undef unless $res;
    $res->[ field_source ] = &SOURCE_DOWNLOAD;
    $res->[ field_url ]    = undef;
    return $res;
} ## end sub make_download($$ )

sub make_dummy($$ ) {
    my ( $class, $name ) = @_;
    $class->load_database unless $loaded;
    my $res = $class->p_find_entry( $name );
    return $class->p_make_entry( $name, record_type_dummy );
} ## end sub make_dummy($$ )

sub make_alias($$@ ) {
    my ( $class, $alias, @names ) = @_;
    $class->load_database unless $loaded;
    my $res = $class->p_make_entry( $alias, record_type_alias );
    return undef unless $res;
    my $names = [];
    while ( @names ) {
        my $name = shift @names;
        if ( ref $name ) {
            unshift @names, @$name;
            next;
        }
        push @$names, $name;
    } ## end while ( @names )
    $res->[ field_alias ] = $names;
    return $res;
} ## end sub make_alias($$@ )

sub remake_alias($$@ ) {
    my ( $class, $alias, @names ) = @_;
    $class->load_database unless $loaded;
    my $res = $class->p_find_entry( $alias );
    $res->p_clear if ( $res && $res->is_alias );
    return $class->make_alias( $alias, @names );
} ## end sub remake_alias($$@ )

sub load_database($ ) {
    my $class = shift;
    return if $loaded;
    $loaded = 1;

    my %preloaded = ();
    my %db_vers   = ();
    my %seen_keys = ();

    foreach my $database_name ( "addons", "groups", "aliases", "splits", "embeds", "libraries" ) {
        my $preload = $preloaded{ $database_name } ||= [];
        my $database = database_file( $database_name );
        next unless ( -f $database && -r _ );
        my $fh = new IO::File $database, "r";
        next unless $fh;
        my $version = 1;
        $version = 5 if ( $database_name ne "addons" );
        while ( <$fh> ) {

            if ( m{^\s*#\s*DatabaseVersion:\s*(\d+)$}i ) {
                $version = $1;
                next;
            }
            next if $_ =~ m{^\s*#};
            chomp;
            s{\r}{} if ( $^O eq "MSWin32" );
            my @line = ( shellwords( $_ ) );
            next unless @line;
            local @ARGV = @line;
            my $addon_opts = {};

            unless (
                GetOptions(
                    $addon_opts,
                    "root=s",      # Obsolete
                    "branch=s",    # Obsolete
                    "svn=s",
                    "git=s",
                    "hg=s",
                    "manual",
                    "subdirs!",    # Obsolete
                    "group=s",
                    "subdir=s",    # Obsolete
                    "embed|embedded|extern",
                    "disembed|disembedded|noextern",
                    "nosplit",
                    "cansplit",
                    "enclosure",
                    "noenclosure",
                    "name=s",      # Obsolete
                    "alias=s@",
                    "require=s@",
                    "split=s@",
                    "embedurl=s",
                    "libname=s@",
                    "ignore!",
                    "dead!",
                    "internal!",
                    "override!",
                    "dynamic!"     # Obsolete
                )
                )
            {
                die "Unable to parse database line: " . $_;
            } ## end unless ( GetOptions( $addon_opts...))
            $addon_opts->{ "args" } = [ @ARGV ];
            push @$preload, $addon_opts;

            #### Figure out seen keys
            my @consider = ();
            if ( defined $addon_opts->{ "group" } ) {
                push @consider, $addon_opts->{ "group" };
            }
            else {
                push @consider, @ARGV;
                if ( $addon_opts->{ "alias" } ) {
                    my $aliases = $addon_opts->{ "alias" };
                    push @consider, @$aliases;
                }
                if ( $addon_opts->{ "split" } ) {
                    my $splits = $addon_opts->{ "split" };
                    push @consider, @$splits;
                }
            } ## end else [ if ( defined $addon_opts...)]
            foreach my $name ( @consider ) {
                my $key = addon_name_to_key( $name );
                $seen_keys{ $key } = 1 if $key;
            }
        } ## end while ( <$fh> )
        $fh->close;
        undef $fh;
        $db_vers{ $database_name } = $version;
    } ## end foreach my $database_name (...)

    my $force_cache;
    my $weak_force;
    if ( $opt_actions{ "fetch" } ) {
        $weak_force = 1;
        foreach my $name ( @opt_args ) {
            my $key = addon_name_to_key( $name );
            $force_cache = 1 unless $seen_keys{ $key };
        }
        $force_cache = 1 if $opt_source{ "libs" };
    } ## end if ( $opt_actions{ "fetch"...})

    while ( my ( $section, $value ) = each %defaults ) {
        my $fields      = $value->{ &default_fields };
        my $addons      = $value->{ &default_addons };
        my $hide_addons = $value->{ &default_hide_addons };
        my $dummy       = $value->{ &default_dummy };
        my $group       = $value->{ &default_group };
        my $require     = $value->{ &default_require };
        my $aliases     = $value->{ &default_alias };
        my $split       = $value->{ &default_split };
        my $libname     = $value->{ &default_libname };
        my $can_cache   = $value->{ &default_can_cache };
        if ( $can_cache ) {
            my $uri = format_string(
                $fields->{ &field_svn_url },
                "name"   => "",
                "branch" => "trunk/"
            );
            my $cache = get_svn_directory_cache( $uri, $force_cache, $weak_force );
            if ( ref $cache ) {
                $addons = [];
                foreach my $rec ( values %$cache ) {
                    next if $rec->{ "kind" } && $rec->{ "kind" } ne "dir";
                    push @$addons, $rec->{ "name" };
                }
            } ## end if ( ref $cache )
        } ## end if ( $can_cache )
        $group = "" unless defined $group;
        my $group_rec;
        $group_rec = ( $groups_by_name{ $group } ||= {} ) if $group ne "";
        my $given_url = $fields->{ &field_url };
        my %hide_addons;
        if ( $hide_addons ) {
            %hide_addons = map { $_ => 1 } @$hide_addons;
        }
        foreach my $addon_full ( @$addons ) {
            my $addon  = $addon_full;
            my $prefix = $addon;
            if ( $addon_full =~ m{^(.*)::(.*)$} ) {
                $addon  = $2;
                $prefix = $1;
            }
            elsif ( $addon_full =~ m{^(.*):(.*)$} ) {
                $addon  = $1 . $2;
                $prefix = $1;
            }
            elsif ( $addon_full =~ m{^(.*)_} ) {
                $prefix = $1;
            }
            if ( defined $given_url ) {
                $fields->{ &field_url } = format_string(
                    $given_url,
                    "prefix" => $prefix
                );
            } ## end if ( defined $given_url)
            next if $hide_addons{ $addon };
            my $rec = $class->p_make_entry(
                $addon, record_type_addon,
                &field_internal => 1,
                %$fields
            );
            if ( $rec && $group ne "" ) {
                my $key = $rec->[ field_key ];
                $group_rec->{ $key } = $addon;
                $groups_by_addon{ $key } = $group;
            }
        } ## end foreach my $addon_full ( @$addons)
        foreach my $dummy ( @$dummy ) {
            $class->p_make_entry(
                $dummy, record_type_dummy,
                &field_internal => 1
            );
        } ## end foreach my $dummy ( @$dummy)
        if ( $require ) {
            while ( my ( $addon, $requires ) = each %$require ) {
                my $info = $class->p_find_entry( $addon );
                $info->add_require( $requires ) if $info;
            }
        } ## end if ( $require )
        if ( $aliases ) {
            while ( my ( $alias, $name ) = each %$aliases ) {
                my $info = $class->make_alias( $alias, $name );
                $info->[ field_internal ] = 1;
            }
        } ## end if ( $aliases )
        if ( $split ) {
            while ( my ( $addon, $splits ) = each %$split ) {
                my $info = $class->p_find_entry( $addon );
                $info->add_split( $splits ) if $info;
            }
        } ## end if ( $split )
        if ( $libname ) {
            while ( my ( $addon, $libname ) = each %$libname ) {
                my $info = $class->p_find_entry( $addon );
                $info->add_libname( $libname ) if $info;
            }
        } ## end if ( $libname )
    } ## end while ( my ( $section, $value...))

    foreach my $database_name ( keys %preloaded ) {
        my $preload = $preloaded{ $database_name };
        my $version = $db_vers{ $database_name };
        foreach my $addon_opts ( @$preload ) {
            local @ARGV = @{ $addon_opts->{ "args" } };
            if (   $addon_opts->{ "internal" }
                && $addon_opts->{ "svn" }
                && $addon_opts->{ "svn" } =~ m{//svn.wowace.com/wowace/trunk/}
                && !$addon_opts->{ "override" } )
            {

                # Disable old automatic entries for wowace addons
                undef $addon_opts->{ "internal" };
                $addon_opts->{ "dead" } = 1;
            } ## end if ( $addon_opts->{ "internal"...})
            if ( defined $addon_opts->{ "group" } ) {
                my $group = $addon_opts->{ "group" };
                my $group_rec = ( $groups_by_name{ $group } ||= {} );
                while ( @ARGV ) {
                    my $addon     = path_to_node( shift @ARGV );
                    my $key       = addon_name_to_key( $addon );
                    my $old_group = $groups_by_addon{ $key };
                    if ( defined $old_group ) {
                        next if $old_group eq $group;
                        my $old_rec = $groups_by_name{ $old_group };
                        delete $old_rec->{ $key };
                    }
                    $groups_by_addon{ $key } = $group;
                    $group_rec->{ $key } = $addon;
                } ## end while ( @ARGV )
                next;
            } ## end if ( defined $addon_opts...)
            if ( defined $addon_opts->{ "alias" } ) {
                my $aliases = $addon_opts->{ "alias" };
                my @alias_names;
                my @alias_addons;
                if ( $version < 2 ) {

                    # aliasname -alias addonone -alias addontwo -alias ...
                    @alias_names  = @ARGV;
                    @alias_addons = @$aliases;
                } ## end if ( $version < 2 )
                else {

                    # -alias aliasname addonone addontwo ...
                    @alias_names  = @$aliases;
                    @alias_addons = @ARGV;
                } ## end else [ if ( $version < 2 ) ]
                foreach my $name ( @alias_names ) {
                    my $info = $class->p_find_entry( $name );
                    my $was_internal = $info && $info->is_internal;
                    next if ( ( $addon_opts->{ "internal" } || $was_internal ) && !$addon_opts->{ "override" } );
                    $info = $class->remake_alias( $name, @alias_addons );
                    if ( $info ) {
                        $info->p_make_internal if $was_internal;
                        $info->p_make_override if $addon_opts->{ "override" };
                    }
                } ## end foreach my $name ( @alias_names)
                next;
            } ## end if ( defined $addon_opts...)
            if ( defined $addon_opts->{ "split" } ) {
                my $splits = $addon_opts->{ "split" };
                my @base_names;
                my @split_names;
                my $no_next;
                if ( $version < 3 ) {

                    # basename <other options> -split splitname -split splitname ...
                    @base_names  = @ARGV;
                    @split_names = @$splits;
                    $no_next     = 1;
                } ## end if ( $version < 3 )
                else {

                    # -split basename splitname splitname ...
                    @base_names  = @$splits;
                    @split_names = @ARGV;
                } ## end else [ if ( $version < 3 ) ]
                foreach my $name ( @base_names ) {
                    add_split( $name, @split_names );
                    if ( $version > 3 ) {
                        if ( defined $addon_opts->{ "nosplit" } ) {
                            disallow_split( $name );
                        }
                        elsif ( defined $addon_opts->{ "cansplit" } ) {
                            allow_split( $name );
                        }
                    } ## end if ( $version > 3 )
                } ## end foreach my $name ( @base_names)
                next unless $no_next;
            } ## end if ( defined $addon_opts...)
            if ( $version > 3 ) {
                if ( defined $addon_opts->{ "noenclosure" } ) {

                    # Obsolete
                    next;
                }
                elsif ( defined $addon_opts->{ "enclosure" } ) {

                    # Obsolete
                    next;
                }
                elsif ( defined $addon_opts->{ "nosplit" } ) {
                    disallow_split( "" );
                    next;
                }
                elsif ( defined $addon_opts->{ "cansplit" } ) {
                    allow_split( "" );
                    next;
                }
            } ## end if ( $version > 3 )
            if ( $version > 4 ) {
                if ( defined $addon_opts->{ "disembed" } ) {
                    if ( @ARGV ) {
                        foreach my $name ( @ARGV ) {
                            disembed_externals( $name );
                        }
                    }
                    else {
                        disembed_externals( "" );
                    }
                    next;
                } ## end if ( defined $addon_opts...)
                elsif ( defined $addon_opts->{ "embed" } ) {
                    if ( @ARGV ) {
                        foreach my $name ( @ARGV ) {
                            embed_externals( $name );
                        }
                    }
                    else {
                        embed_externals( "" );
                    }
                    next;
                } ## end elsif ( defined $addon_opts...)
                elsif ( defined $addon_opts->{ "embedurl" } ) {
                    my $name = $addon_opts->{ "embedurl" };
                    add_embed_urls( $name, @ARGV );
                    next;
                }
            } ## end if ( $version > 4 )
            if ( defined $addon_opts->{ "libname" } ) {
                my $libnames = $addon_opts->{ "libname" };
                my @base_names;
                my @libnames;
                my $no_next;
                if ( $version < 3 ) {

                    # basename <other options> -libname libname -libname libname ...
                    @base_names = @ARGV;
                    @libnames   = @$libnames;
                    $no_next    = 1;
                } ## end if ( $version < 3 )
                else {

                    # -libname basename libname libname ...
                    @base_names = @$libnames;
                    @libnames   = @ARGV;
                } ## end else [ if ( $version < 3 ) ]
                foreach my $name ( @base_names ) {
                    add_libname( $name, @libnames );
                }
                next unless $no_next;
            } ## end if ( defined $addon_opts...)
            next unless @ARGV;
            if ( !$addon_opts->{ "ignore" } && defined $addon_opts->{ "name" } && @ARGV > 1 ) {
                die "Unable to parse database line: " . $_ . ": to many addons map to one name.\n";
            }
            while ( @ARGV ) {
                my $addon = path_to_node( shift @ARGV );
                $addon = &DIR_OSX_ICON if $addon eq "%OSXICON%";
                my $info = $class->p_find_entry( $addon );
                my $was_internal = $info && $info->is_internal;
                next if ( ( $addon_opts->{ "internal" } || $was_internal ) && !$addon_opts->{ "override" } );
                $class->p_clear_entry( $addon ) if $info;
                undef $info;
                if ( $addon_opts->{ "ignore" } ) {
                    $info = $class->make_dummy( $addon );
                }
                else {
                    my $svn_url;
                    my $git_url;
                    my $hg_url;
                    if ( $addon_opts->{ "svn" } ) {
                        $svn_url = $addon_opts->{ "svn" };
                    }
                    elsif ( $addon_opts->{ "git" } ) {
                        $git_url = $addon_opts->{ "git" };
                    }
                    elsif ( $addon_opts->{ "hg" } ) {
                        $hg_url = $addon_opts->{ "hg" };
                    }
                    elsif ( $addon_opts->{ "root" } ) {

                        # Old style url
                        $svn_url = $addon_opts->{ "root" };
                        $svn_url =~ s{%NAME%}{%Name%}g;
                        if ( $addon_opts->{ "branch" } ) {
                            $svn_url .= $addon_opts->{ "branch" };
                        }
                        else {
                            $svn_url .= "trunk/";
                        }
                        my $addon_name = defined $addon_opts->{ "name" } ? $addon_opts->{ "name" } : $addon;
                        if ( $addon_name eq $addon ) {
                            $svn_url .= "%Name%";
                        }
                        elsif ( $addon_name eq lc $addon ) {
                            $svn_url .= "%name%";
                        }
                        elsif ( $addon_name eq uc $addon ) {
                            $svn_url .= "%NAME%";
                        }
                        else {
                            $svn_url .= $addon_name;
                        }
                    } ## end elsif ( $addon_opts->{ "root"...})
                    if ( defined $hg_url ) {
                        $info = $class->make_hg_url( $addon, $hg_url );
                    }
                    elsif ( defined $git_url ) {
                        $info = $class->make_git_url( $addon, $git_url );
                    }
                    elsif ( defined $svn_url ) {
                        $info = $class->make_svn_url( $addon, $svn_url );
                    }
                    elsif ( $addon_opts->{ "manual" } ) {
                        $info = $class->make_download( $addon );
                    }
                    else {
                        next;
                    }
                    next unless $info;
                    $info->disallow_split     if $addon_opts->{ "nosplit" };
                    $info->allow_split        if $addon_opts->{ "cansplit" };
                    $info->embed_externals    if $addon_opts->{ "embed" };
                    $info->disembed_externals if $addon_opts->{ "disembed" };
                } ## end else [ if ( $addon_opts->{ "ignore"...})]
                next unless $info;
                $info->p_make_internal                           if $was_internal;
                $info->p_make_override                           if $addon_opts->{ "override" };
                $info->set_group( $addon_opts->{ "subdir" } )    if $addon_opts->{ "subdir" };
                $info->add_require( $addon_opts->{ "require" } ) if $addon_opts->{ "require" };
                $info->set_dead( $addon_opts->{ "dead" } )       if defined $addon_opts->{ "dead" };
            } ## end while ( @ARGV )
        } ## end foreach my $addon_opts ( @$preload)
        if ( defined $version && $version < 5 ) {
            warn "Upgrading " . $database_name . " database to version 5.\n";
        }
    } ## end foreach my $database_name (...)
} ## end sub load_database($ )

sub save_database($ ) {
    return unless $loaded;

    my $addons_db = <<END_OF_DB_HEADER;
# Addon database for addonpkg
# Syntax: addonname [options]
# Options:
#   -svn [url]      Sets the SVN URL.
#   -git [url]      Sets the GIT URL.
#   -hg  [url]      Sets the Mercurial URL.
#   -manual         Marks a manually downloaded addon.
#   -name [name]    Named used when fetching this addon.
#   -require [name] Addon name is required for this addon. Required addons
#                   will be included automatically when the -libs option is
#                   used. May appear more than once.
#   -ignore         Addonpkg should ignore any addon with this name.
#   -override       Override addonpkg internal database for this addon.
#   -internal       This line comes from the internal database and will
#                   be ignored unless -override is specified.
#   -dead           Marks an addon as unfetchable.
# NOTE this file is rewritten every time addonpkg exits.
# DatabaseVersion: 5
END_OF_DB_HEADER

    my $aliases_db = <<END_OF_ALIAS_HEADER;
# Aliases
# Syntax: -alias [name] [-internal] [-override] addons
#   Makes name an alias for the listed addons. Aliases may refer to
#   other aliases, groups and even full paths.
# NOTE this file is rewritten every time addonpkg exits.
# DatabaseVersion: 5
END_OF_ALIAS_HEADER

    my $splits_db = <<END_OF_SPLITS_HEADER;
# Split syntax: -split [base] [options] addons
#   Names the sub addons that split off of the base addon.
#   Split options:
#      -cansplit          Allow splitting even if default is disabled
#      -nosplit           Disallow splitting even if default is enabled
# NOTE this file is rewritten every time addonpkg exits.
# DatabaseVersion: 5
END_OF_SPLITS_HEADER

    my $embeds_db = <<END_OF_EMBEDS_HEADER;
# Embed syntax:
#   -embed       addons   Fetch embeds inside addons even if not default
#   -disembed    addons   Do not fetch embeds inside addons, even if default
# NOTE this file is rewritten every time addonpkg exits.
# DatabaseVersion: 5
END_OF_EMBEDS_HEADER

    my $libraries_db = <<END_OF_LIBRARIES_HEADER;
# Libname syntax: -libname [base] libnames
#   Alternate names for libraries. Used to handle dependencies when
#   the library name is not the same as the addon name.
# NOTE this file is rewritten every time addonpkg exits.
# DatabaseVersion: 5
END_OF_LIBRARIES_HEADER

    if ( is_split_allowed( "", 0 ) ) {
        $splits_db .= <<END_OF_CANSPLIT;

# Default for splitting
-cansplit
END_OF_CANSPLIT
    } ## end if ( is_split_allowed(...))
    elsif ( !is_split_allowed( "", 1 ) ) {
        $splits_db .= <<END_OF_NOSPLIT;

# Default for splitting
-nosplit
END_OF_NOSPLIT
    } ## end elsif ( !is_split_allowed...)

    if ( are_externals_embedded( "", 0 ) ) {
        $embeds_db .= <<END_OF_EMBED;

# Default for embed
-embed
END_OF_EMBED
    } ## end if ( are_externals_embedded...)
    elsif ( !are_externals_embedded( "", 1 ) ) {
        $embeds_db .= <<END_OF_DISEMBED;

# Default for embed
-disembed
END_OF_DISEMBED
    } ## end elsif ( !are_externals_embedded...)

    my $last_url  = "zzzz";
    my $last_type = "zzzz";
    foreach my $ref ( sort { return ( 0 || ( ( $a->[ field_record_type ] || "ZZZZ" ) cmp( $b->[ field_record_type ] || "ZZZZ" ) ) || ( ( $a->[ field_source ] || "ZZZZ" ) cmp( $b->[ field_source ] || "ZZZZ" ) ) || ( ( $a->[ field_url ] || "ZZZZ" ) cmp( $b->[ field_url ] || "ZZZZ" ) ) || ( ( $a->[ field_key ] || "ZZZZ" ) cmp( $b->[ field_key ] || "ZZZZ" ) ) || 0 ) } values %addons_by_key ) {
        next if $ref->is_assumption;
        my $name        = $ref->[ field_name ];
        my $simple_name = $name;
        my $type        = $ref->[ field_record_type ] || "";
        my $new_type    = ( $last_type ne $type );
        my $prev_type   = $last_type;
        $last_type = $type;
        $simple_name = "%OSXICON%" if ( $name eq &DIR_OSX_ICON );

        my @addon_line   = ();
        my @alias_line   = ();
        my @split_line   = ();
        my @embed_line   = ();
        my @embed_line2  = ();
        my @libname_line = ();
        my @mid_line     = ();

        push @mid_line, "-override" if $ref->is_override;
        push @mid_line, "-internal" if $ref->is_internal;
        push @mid_line, "-dead"     if $ref->is_dead;

        if ( $type eq record_type_alias ) {
            if ( $new_type ) {
            }
            push @alias_line, "-alias", $simple_name, @mid_line;
            push @alias_line, sort $ref->get_alias;
        } ## end if ( $type eq record_type_alias)
        elsif ( $type eq record_type_dummy ) {
            if ( $new_type ) {
                $addons_db .= "\n# The following addon names are ignored.\n";
            }
            push @addon_line, $simple_name, @mid_line, "-ignore";
        } ## end elsif ( $type eq record_type_dummy)
        elsif ( $type eq record_type_addon ) {
            push @addon_line, $simple_name, @mid_line;
            if ( $ref->[ field_source ] eq &SOURCE_SVN ) {
                my $svn_url = $ref->[ field_url ];
                if ( $svn_url ne $last_url || $new_type ) {
                    $addons_db .= "\n# The following addons will come from " . p_quote_word( $svn_url ) . "\n";
                }
                push @addon_line, "-svn", $svn_url;
                $last_url = $svn_url;
            } ## end if ( $ref->[ field_source...])
            elsif ( $ref->[ field_source ] eq &SOURCE_GIT ) {
                my $git_url = $ref->[ field_url ];
                if ( $git_url ne $last_url || $new_type ) {
                    $addons_db .= "\n# The following addons will come from " . p_quote_word( $git_url ) . "\n";
                }
                push @addon_line, "-git", $git_url;
                $last_url = $git_url;
            } ## end elsif ( $ref->[ field_source...])
            elsif ( $ref->[ field_source ] eq &SOURCE_HG ) {
                my $hg_url = $ref->[ field_url ];
                if ( $hg_url ne $last_url || $new_type ) {
                    $addons_db .= "\n# The following addons will come from " . p_quote_word( $hg_url ) . "\n";
                }
                push @addon_line, "-hg", $hg_url;
                $last_url = $hg_url;
            } ## end elsif ( $ref->[ field_source...])
            elsif ( $ref->[ field_source ] eq &SOURCE_DOWNLOAD ) {
                if ( $new_type ) {
                    $addons_db .= "\n# The following addons have been manually downloaded\n";
                }
                push @addon_line, "-manual";
                $last_url = "";
            } ## end elsif ( $ref->[ field_source...])
            foreach my $require ( sort $ref->expand_require ) {
                push @addon_line, "-require", $require;
            }
            my @splits = sort $ref->get_splits;
            unshift @splits, "-cansplit" if $ref->is_split_allowed( 0 );
            unshift @splits, "-nosplit" unless $ref->is_split_allowed( 1 );
            if ( @splits ) {
                @split_line = ( "-split", $simple_name, @splits );
            }
            my @embed_urls = sort $ref->get_embed_urls;
            push @embed_line, "-embed", $simple_name if $ref->are_externals_embedded( 0 );
            push @embed_line, "-disembed", $simple_name unless $ref->are_externals_embedded( 1 );
            if ( @embed_urls ) {
                push @embed_line2, "-embedurl", $simple_name, @embed_urls;
            }
            my @libnames = sort $ref->get_libnames;
            if ( @libnames ) {
                @libname_line = ( "-libname", $simple_name, @libnames );
            }
        } ## end elsif ( $type eq record_type_addon)
        else {
        }
        $addons_db    .= join( " ", map { p_quote_word( $_ ) } @addon_line ) . "\n"   if @addon_line;
        $aliases_db   .= join( " ", map { p_quote_word( $_ ) } @alias_line ) . "\n"   if @alias_line;
        $splits_db    .= join( " ", map { p_quote_word( $_ ) } @split_line ) . "\n"   if @split_line;
        $embeds_db    .= join( " ", map { p_quote_word( $_ ) } @embed_line ) . "\n"   if @embed_line;
        $embeds_db    .= join( " ", map { p_quote_word( $_ ) } @embed_line2 ) . "\n"  if @embed_line2;
        $libraries_db .= join( " ", map { p_quote_word( $_ ) } @libname_line ) . "\n" if @libname_line;
    } ## end foreach my $ref ( sort { return...})

    my $groups_db = <<END_OF_GROUP_HEADER;
# Addon groups
# Syntax: -group [subdirectory] addons
#   Store the listed addons in the name subdirectory. The subdirectory
#   name may be used as an argument to addonpkg to operate with all
#   the addons currently in that subdirectory.
# NOTE this file is rewritten every time addonpkg exits.
# DatabaseVersion: 5

END_OF_GROUP_HEADER

    foreach my $group ( sort keys %groups_by_name ) {
        my $pos       = 100;
        my $group_rec = $groups_by_name{ $group };
        my @addons    = sort values %$group_rec;
        my $header    = "-group " . p_quote_word( $group );

        unless ( @addons ) {
            $groups_db .= $header . "\n";
            next;
        }
        while ( @addons ) {
            my $ptext = " " . p_quote_word( shift @addons );
            if ( $pos + length( $ptext ) > 78 ) {
                $groups_db .= "\n" . $header;
                $pos = length( $header );
            }
            $groups_db .= $ptext;
            $pos += length( $ptext );
        } ## end while ( @addons )
        $groups_db .= "\n";
    } ## end foreach my $group ( sort keys...)
    update_database_file( "addons",    $addons_db );
    update_database_file( "groups",    $groups_db );
    update_database_file( "aliases",   $aliases_db );
    update_database_file( "splits",    $splits_db );
    update_database_file( "embeds",    $embeds_db );
    update_database_file( "libraries", $libraries_db );
} ## end sub save_database($ )

## Instance Methods
### Private
sub p_is_weaker_than($$ ) {
    my ( $this, $type ) = @_;
    return 1 if $this->is_assumption;
    my $old_type = $this->get_record_type;
    my $old_rank = ( $record_type_rank{ $old_type } || 0 );
    my $new_rank = ( $record_type_rank{ $type } || 0 );
    return ( $old_rank > $new_rank );
} ## end sub p_is_weaker_than($$ )

sub p_normalize_url($ ) {
    my $this = shift;
    return format_string(
        $this->[ field_url ],
        "name"   => $this->get_name,
        "branch" => "trunk/"
    );
} ## end sub p_normalize_url($ )

sub p_expand_field($$ ) {
    my ( $this, $field ) = @_;
    my $val = $this->[ $field ];
    return ( defined $val ? values %$val : () );
}

sub p_add_to_field($$@ ) {
    my ( $this, $field, @names ) = @_;
    my $val;
    while ( @names ) {
        my $name = shift @names;
        if ( ref $name ) {
            unshift @names, @$name;
            next;
        }
        my $name_key = addon_name_to_key( $name );
        $val = ( $this->[ $field ] ||= {} ) unless $val;
        $val->{ $name_key } = $name;
    } ## end while ( @names )
    return $this;
} ## end sub p_add_to_field($$@ )

### Public
sub get_url($;$ ) {
    my ( $this, $required_type ) = @_;
    die "Internal error: " . $this->get_name . ": is not an addon.\n"
        unless ( $this->get_record_type eq &record_type_addon );
    if ( defined $required_type ) {
        die "Internal error: " . $this->get_name . ": is not an " . $source_types_to_name{ $required_type } . " addon.\n"
            unless ( $this->get_source_type eq $required_type );
    }
    return $this->p_normalize_url;
} ## end sub get_url($;$ )

sub get_base_addon($ ) {
    my $this     = shift;
    my $base_key = $this->[ field_base_key ];
    return undef unless defined $base_key;
    my $rec = $addons_by_key{ $base_key };
    return $base_key unless defined $rec;
    return $rec->[ field_name ];
} ## end sub get_base_addon($ )

sub get_base_addoninfo($ ) {
    my $this     = shift;
    my $base_key = $this->[ field_base_key ];
    return undef unless defined $base_key;
    my $rec = $addons_by_key{ $base_key };
    return $rec;
} ## end sub get_base_addoninfo($ )

sub set_externals_embedded($$ ) {
    my ( $this, $state ) = @_;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    undef $state if ( defined $state && $state =~ m{^default$}i );
    if ( $this_key eq "" && !defined $state ) {

        # Remove all defaults
        $state                     = $externals_embedded_by_key{ $this_key };
        %externals_embedded_by_key = ();
    } ## end if ( $this_key eq "" &&...)
    $externals_embedded_by_key{ $this_key } = $state;
} ## end sub set_externals_embedded($$ )

sub are_externals_embedded($;$ ) {
    my ( $this, $default ) = @_;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    my $state = $externals_embedded_by_key{ $this_key };
    if ( !defined $state ) {
        if ( !defined $default || $default =~ m{^default$}i ) {

            # No state, use the system "default" state
            $state = are_externals_embedded( "", 0 );
        }
        else {

            # No state, use the given state
            $state = $default;
        }
    } ## end if ( !defined $state )
    return $state;
} ## end sub are_externals_embedded($;$ )

sub set_split_allowed($$ ) {
    my ( $this, $state ) = @_;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    undef $state if ( defined $state && $state =~ m{^default$}i );
    if ( $this_key eq "" && !defined $state ) {

        # Remove all defaults
        $state                = $split_allowed_by_key{ $this_key };
        %split_allowed_by_key = ();
    } ## end if ( $this_key eq "" &&...)
    $split_allowed_by_key{ $this_key } = $state;
} ## end sub set_split_allowed($$ )

sub is_split_allowed($;$ ) {
    my ( $this, $default ) = @_;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    my $state = $split_allowed_by_key{ $this_key };
    if ( !defined $state ) {
        if ( !defined $default || $default =~ m{^default$}i ) {

            # No state, use the system "default" state
            $state = is_split_allowed( "", 1 );
        }
        else {

            # No state, use the given state
            $state = $default;
        }
    } ## end if ( !defined $state )
    return $state;
} ## end sub is_split_allowed($;$ )

sub add_split($;@ ) {
    my ( $this, @names ) = @_;
    my $splits;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    while ( @names ) {
        my $name = shift @names;
        if ( ref $name ) {
            unshift @names, @$name;
            next;
        }
        my $name_key = addon_name_to_key( $name );
        my $other    = $addons_by_key{ $name_key };
        if ( $other ) {
            next unless ( $other->is_split
                || $other->p_is_weaker_than( record_type_split ) );
            $other->p_clear;
        }
        $splits = ( $splitoffs_by_key{ $this_key } ||= {} ) unless $splits;
        $splits->{ $name_key } = $name;
        AddonInfo->p_make_entry(
            $name, record_type_split,
            &field_base_key => $this_key
        );
    } ## end while ( @names )
    return $this;
} ## end sub add_split($;@ )

sub get_splits($ ) {
    my $this     = shift;
    my $this_key = $this->[ field_key ];
    my $splits   = $splitoffs_by_key{ $this_key };
    return ( defined $splits ? values %$splits : () );
} ## end sub get_splits($ )

sub add_embed_urls($;@ ) {
    my ( $this, @urls ) = @_;
    my $embed_urls;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    while ( @urls ) {
        my $url = shift @urls;
        if ( ref $url eq q{ARRAY} ) {
            unshift @urls, @$url;
            next;
        }
        my $uri = URI->new( $url )->canonical;
        $embed_urls //= ( $must_embed_urls_by_key{ $this_key } //= {} );
        $embed_urls->{ $uri } = 1;
    } ## end while ( @urls )
    return $this;
} ## end sub add_embed_urls($;@ )

sub remove_embed_urls($;@ ) {
    my ( $this, @urls ) = @_;
    my $embed_urls;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    while ( @urls ) {
        my $url = shift @urls;
        if ( ref $url eq q{ARRAY} ) {
            unshift @urls, @$url;
            next;
        }
        my $uri = URI->new( $url )->canonical;
        $embed_urls //= ( $must_embed_urls_by_key{ $this_key } //= {} );
        delete $embed_urls->{ $uri };
    } ## end while ( @urls )
    return $this;
} ## end sub remove_embed_urls($;@ )

sub get_embed_urls($ ) {
    my $this       = shift;
    my $this_key   = $this->[ field_key ];
    my $embed_urls = $must_embed_urls_by_key{ $this_key };
    return ( defined $embed_urls ? keys %$embed_urls : () );
} ## end sub get_embed_urls($ )

sub add_libname($;@ ) {
    my ( $this, @names ) = @_;
    my $libnames;
    my $this_key = ref $this ? $this->[ field_key ] : addon_name_to_key( $this );
    while ( @names ) {
        my $name = shift @names;
        if ( ref $name ) {
            unshift @names, @$name;
            next;
        }
        my $name_key = addon_name_to_key( $name );
        my $other    = $addons_by_key{ $name_key };
        if ( $other ) {
            next unless ( $other->is_libname
                || $other->p_is_weaker_than( record_type_libname ) );
            $other->p_clear;
        }
        $libnames = ( $libnames_by_key{ $this_key } ||= {} ) unless $libnames;
        $libnames->{ $name_key } = $name;
        AddonInfo->p_make_entry(
            $name, record_type_libname,
            &field_base_key => $this_key
        );
    } ## end while ( @names )
    return $this;
} ## end sub add_libname($;@ )

sub get_libnames($ ) {
    my $this     = shift;
    my $this_key = $this->[ field_key ];
    my $libnames = $libnames_by_key{ $this_key };
    return ( defined $libnames ? values %$libnames : () );
} ## end sub get_libnames($ )

sub get_group($ ) {
    my $addon = shift;
    AddonInfo->load_database unless $loaded;
    my $key = ref( $addon ) ? $addon->[ field_key ] : addon_name_to_key( $addon );
    my $group = $groups_by_addon{ $key } || "";
    undef $group if $group eq "";
    return $group;
} ## end sub get_group($ )

sub set_group($$ ) {
    my $addon = shift;
    my ( $group ) = ( File::Spec->no_upwards( @_ ) );
    $group = "" unless defined $group;
    AddonInfo->load_database unless $loaded;
    my $key = ref( $addon ) ? $addon->[ field_key ] : addon_name_to_key( $addon );
    use Carp;
    $addon = $addons_by_key{ $key } if $addons_by_key{ $key };
    my $name = ref( $addon ) ? $addon->[ field_name ] : $addon;
    my $old_group = $groups_by_addon{ $key };

    if ( defined $old_group ) {
        return if $old_group eq $group;
        my $old_rec = $groups_by_name{ $old_group };
        delete $old_rec->{ $key };
    } ## end if ( defined $old_group)
    $groups_by_addon{ $key } = $group;
    my $group_rec = ( $groups_by_name{ $group } ||= {} );
    $group_rec->{ $key } = $name;
} ## end sub set_group($$ )

sub clear_dead_as_svn($$ ) {
    my ( $this, $url ) = @_;
    $this->[ field_is_dead ] = 0;
    $this->[ field_source ]  = &SOURCE_SVN;
    $this->[ field_svn_url ] = $url;
    return $this;
} ## end sub clear_dead_as_svn($$ )

sub clear_dead_as_git($$ ) {
    my ( $this, $url ) = @_;
    $this->[ field_is_dead ] = 0;
    $this->[ field_source ]  = &SOURCE_GIT;
    $this->[ field_git_url ] = $url;
    return $this;
} ## end sub clear_dead_as_git($$ )

sub clear_dead_as_hg($$ ) {
    my ( $this, $url ) = @_;
    $this->[ field_is_dead ] = 0;
    $this->[ field_source ]  = &SOURCE_HG;
    $this->[ field_hg_url ]  = $url;
    return $this;
} ## end sub clear_dead_as_hg($$ )

1;
